// Copyright 2011 Jay Young. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 *
 * A component that manages the main navigation menu.
 */

goog.provide('rb.menu.Menu');

goog.require('goog.Disposable');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.classes');
goog.require('goog.events.EventHandler');
goog.require('goog.string');

goog.require('rb.auth.AuthLevel');
goog.require('rb.menu.templates');
goog.require('rb.post.GetPostsCommand');

goog.require('relief.auth.EventType');
goog.require('relief.nav.EventType');



/**
 * Component which is responsible for the dynamic behavior in the main
 * navigation menu, including showing/hiding the Admin menu, updating the
 * login/logout links based on user auth status, highlighting the selcted
 * blog post, and more.
 *
 * @param {rb.auth.AuthManager} auth The app's auth manager.
 * @param {relief.nav.NavManager} nav The app's nav manager.
 * @param {relief.rpc.RPCService} rpc The app's rpc service.
 * @param {goog.events.EventTarget} eventBus The app's event bus.
 *
 * @constructor
 * @extends {goog.Disposable}
 */
rb.menu.Menu = function(auth, nav, rpc, eventBus) {
  var handler;
  /**
   * @type {goog.events.EventHandler}
   * @private
   */
  this.handler_ = handler = new goog.events.EventHandler(this);

  /**
   * @type {goog.events.EventTarget}
   * @private
   */
  this.eventBus_ = eventBus;

  /**
   * @type {relief.auth.AuthManager}
   * @private
   */
  this.auth_ = auth;
  auth.getCurrentUser(goog.bind(this.gotUser_, this));

  /**
   * @type {relief.nav.NavManager}
   * @private
   */
  this.nav_ = nav;

  /**
   * @type {relief.rpc.RPCService}
   * @private
   */
  this.rpc_ = rpc;

  /**
   * @type {rb.auth.User}
   * @private
   */
  this.user_ = null;

  /**
   * @type {Element}
   * @private
   */
  this.communityMenu_ = goog.dom.getElement('community-menu');

  /**
   * @type {Element}
   * @private
   */
  this.adminMenu_ = goog.dom.getElement('admin-menu');

  /**
   * @type {Element}
   * @private
   */
  this.pageNav_ = goog.dom.getElement('page-nav');

  /**
   * @type {boolean}
   * @private
   */
  this.adminMenuShown_ = false;

  /**
   * @type {Element}
   * @private
   */
  this.postList_ = goog.dom.getElement('menu-post-list');

  /**
   * Flag denoting whether we are currently waiting for the post list to be
   * fetched from the server.
   *
   * @type {boolean}
   * @private
   */
  this.fetchingPostList_ = false;
  this.getPostList_();

  handler.listen(eventBus, relief.auth.EventType.AUTH_CHANGE, this.updateAuth_);
  handler.listen(eventBus, relief.nav.EventType.NAVIGATE, this.onNavigate_);
  handler.listen(eventBus,
                 [rb.post.EventType.CREATED, rb.post.EventType.DISPLAY],
                 this.handlePostEvent_);
};
goog.inherits(rb.menu.Menu, goog.Disposable);


/**
 * Get the list of posts to put into the recent entry list.
 * @private
 */
rb.menu.Menu.prototype.getPostList_ = function() {
  var rpc = this.rpc_,
      onSuccess = goog.bind(this.gotPostList_, this),
      onFailure = goog.bind(this.dontGotPostList_, this),
      cmd = new rb.post.GetPostsCommand(onSuccess, onFailure, 10);

  this.fetchingPostList_ = true;
  rpc.execute(cmd);
};


/**
 * Callback for successful retrieval of recent post summaries.
 *
 * @param {Array.<rb.post.BlogPost>} posts An array of BlogPost summaries.
 * @private
 */
rb.menu.Menu.prototype.gotPostList_ = function(posts) {
  if (posts.length === 0) {
    this.dontGotPostList_();
    return;
  }

  this.fetchingPostList_ = false;

  /**
   * @type {Array.<rb.post.BlogPost>}
   * @private
   */
  this.postData_ = posts;

  var postList = this.postList_,
      templateData = [],
      currentMonth = {
        month: posts[0].getMonthYear(),
        posts: []
      };

  templateData[0] = currentMonth;
  for (var i = 0, len = posts.length; i < len; ++i) {
    var post = posts[i],
        postDate = post.getMonthYear(),
        title = post.getTitle(),
        // Letting titles wrap is ugly.  Ensure a max length that is likely to
        // not break across lines on normal screen sizes.
        displayTitle = post.getShortTitle();

    if (postDate === currentMonth.month) {
      currentMonth.posts.push({
        title: displayTitle,
        slug: post.getSlug()
      });
    }
    else {
      currentMonth = {
        month: postDate,
        posts: [{
          title: displayTitle,
          slug: post.getSlug()
        }]
      };
      templateData.push(currentMonth);
    }
  }

  postList.innerHTML = rb.menu.templates.postList({months: templateData});
  this.highlightLink_(relief.utils.getLocationHash());
};


/**
 * Callback for unsuccessful retrieval of recent post summaries.
 * @private
 */
rb.menu.Menu.prototype.dontGotPostList_ = function() {
  this.fetchingPostList_ = false;
  this.highlightLink_();
};


/**
 * Callback to AuthManager.getCurrentUser().
 * @param {rb.auth.User} user The user.
 * @private
 */
rb.menu.Menu.prototype.gotUser_ = function(user) {
  this.updateAuthMenu_(user);
};


/**
 * Handles authentication changes.
 *
 * @param {relief.auth.AuthChangeEvent} e The auth change event.
 * @private
 */
rb.menu.Menu.prototype.updateAuth_ = function(e) {
  this.updateAuthMenu_(/** @type {rb.auth.User} */ (e.user));
};


/**
 * Updates the state of the main menu.
 *
 * @param {rb.auth.User} user The User to build the menu for.
 * @private
 */
rb.menu.Menu.prototype.updateAuthMenu_ = function(user) {
  var authLevel = user.getAuthLevel();

  var oldUser = this.user_;
  if (oldUser && oldUser.equals(user)) {
    return;
  }

  this.user_ = user;

  if (authLevel > rb.auth.AuthLevel.ANONYMOUS) {
    this.communityMenu_.innerHTML = rb.menu.templates.communityMenuUser({
      name: user.getName()
    });
  }
  else {
    this.communityMenu_.innerHTML = rb.menu.templates.communityMenuNoUser();
  }

  var adminMenu = this.adminMenu_;
  if (authLevel === rb.auth.AuthLevel.ADMIN && !this.adminMenuShown_) {
    adminMenu.innerHTML = rb.menu.templates.adminMenu();
    goog.dom.classes.swap(adminMenu, goog.getCssName('admin-menu-hidden'),
                          goog.getCssName('admin-menu-shown'));
    this.adminMenuShown_ = true;
  }
  else if (authLevel < rb.auth.AuthLevel.ADMIN && this.adminMenuShown_) {
    adminMenu.innerHTML = '';
    goog.dom.classes.swap(adminMenu, goog.getCssName('admin-menu-shown'),
                          goog.getCssName('admin-menu-hidden'));
  }

  this.highlightLink_(relief.utils.getLocationHash());
};


/**
 * Handle DISPLAY PostEvents.
 *
 * @param {rb.post.PostEvent} e The PostEvent.
 * @private
 */
rb.menu.Menu.prototype.handlePostEvent_ = function(e) {
  var post = e.post;
  if (e.type === rb.post.EventType.DISPLAY) {
    this.highlightLink_('#/post/' + post.getSlug());
  }
  else if (e.type === rb.post.EventType.CREATED) {
    var postData = this.postData_;
    if (! postData) {
      /**
       * @type {Array.<rb.post.BlogPost>}
       * @private
       */
      this.postData_ = postData = [];
    }

    goog.array.splice(postData, 0, 0, post);
    this.gotPostList_(postData);
  }
};


/**
 * When the user navigates to a page that does not display a blog post, there
 * is no event to tell us that no post is being shown, so on all navigations we
 * simply clear the highlighting.  As highlighted post titles are cached, this
 * is cheap.
 *
 * @param {relief.nav.NavEvent} e The navigation event.
 * @private
 */
rb.menu.Menu.prototype.onNavigate_ = function(e) {
  var path = '#' + e.path.hash;
  this.highlightLink_(path);
};


/**
 * Highlight the link to the given path.
 *
 * @param {string=} path The path whose link should be highlighted.  If
 *    undefined, any highlighted link will be un-highlighted.
 * @private
 */
rb.menu.Menu.prototype.highlightLink_ = function(path) {
  var pageNav = this.pageNav_,
      highlighted = this.highlighted_,
      viewing = goog.getCssName('nav-list-viewing');

  /*
    States:
      Highlighted link is for the new URL - don't do anything.
      Highlighted link is not for the new URL - unhighlight current link, then
          highlight new link
      No highlighted link - highlight new link
  */

  if (highlighted) {
    if (goog.isDef(path) && goog.string.endsWith(highlighted.href, path)) {
      return;
    }
    else {
      goog.dom.classes.remove(highlighted, viewing);
      highlighted = null;
    }
  }

  // If no path was provided, exit after un-highlighting the current link.
  if (path) {
    var links = goog.dom.getElementsByTagNameAndClass(goog.dom.TagName.A,
                                                      undefined, pageNav);
  
    for (var i = 0, len = links.length; i < len; ++i) {
      var link = links[i];
      if (goog.string.endsWith(link.href, path)) {
        goog.dom.classes.add(link, viewing);
  
        highlighted = link;
        break;
      }
    }
  }
  
  /**
    * @type {Element}
    * @private
    */
   this.highlighted_ = highlighted;
};


/**
 * @inheritDoc
 */
rb.menu.Menu.prototype.disposeInternal = function() {
  this.posts_ = null;

  this.fetchingPostList_ = false;

  this.handler_.dispose();
  this.handler_ = this.auth_ = this.nav_ = this.rpc_ = this.user_ = null;

  this.adminMenu_.innerHTML = '';
  this.communityMenu_ = this.authLink_ = this.adminMenu_ = this.highlighted_ =
      this.pageNav_ = null;
};
